<?php
namespace app\plugins\jwt;
use app\exception\CommonException;
use Firebase\JWT\JWT as BaseJWT;
use Firebase\JWT\Key;
use think\helper\Str;
class Jwt
{

    private $config = null;
    /**
     * 算法类型 HS256、HS384、HS512、RS256、RS384、RS512、ES256、ES384、Ed25519
     * @var string
     */
    private string $algorithms = "HS256";
    /**
     * access_token.
     */
    private const ACCESS_TOKEN = 1;

    /**
     * refresh_token.
     */
    private const REFRESH_TOKEN = 2;

    private string $secret = "@32Azh*7124";

    /**
     * access令牌过期时间，单位：秒。默认 2 小时
     * @var int
     */
    private int $accessExp = 7200;

    /**
     * refresh令牌过期时间，单位：秒。默认 7 天
     * @var string
     */
    private int $refreshExp = 7 * 3600 * 24;

    /**
     * 令牌签发者
     * @var string
     */
    private $iss = "faker";

    /**
     * 某个时间点后才能访问，单位秒。（如：30 表示当前时间30秒后才能使用
     * @var int
     */
    private int $nbf = 3;

    /**
     * 时钟偏差冗余时间，单位秒。建议这个余地应该不大于几分钟
     * @var int
     */
    private int $leeway = 60;

    /**
     * 接收方
     * @var string
     */
    private $aud = "faker";

    /**
     * 全局唯一字段key
     * @var string
     */
    private $uniqKey = "id";

    public function __construct()
    {
        $this->config = app("config");
        $config = $this->config->get('jwt');
        foreach ($config as $key => $val) {
            if (property_exists($this, Str::camel($key))) {
                $this->{Str::camel($key)} = $val;
            }
        }
    }
    public function generateToken(array $data)
    {
        $payload = $this->generatePayload($data);
        if (empty($data[$this->uniqKey])) {
            throw new CommonException("缺少全局唯一key");
        }
        $token = [
            'token_type' => 'Bearer',
            'expires_in' => $this->accessExp,
            'access_token' => $this->makeToken($payload['accessPayload']),
            "refresh_token" => $this->makeToken($payload['refreshPayload']),
        ];
        return $token;
    }

    public function refreshToken()
    {
        $refreshToken = $this->getTokenFromHeaders();
        $data = $this->verifyToken($refreshToken);
        $data['exp'] = time() + $this->refreshExp;
        return ['access_token' => $this->makeToken($data)];
    }

    private function getTokenFromHeader()
    {
        $authorization = request()->header('authorization');
        if (!$authorization || 'undefined' == $authorization) {
            throw new CommonException('身份验证会话已过期，请重新登录！');
        }

        if (self::REFRESH_TOKEN != substr_count($authorization, '.')) {
            throw new CommonException('身份验证会话已过期，请重新登录！');
        }

        if (2 != count(explode(' ', $authorization))) {
            throw new CommonException('Bearer验证中的凭证格式有误，中间必须有个空格');
        }

        [$type, $token] = explode(' ', $authorization);
        if ('Bearer' !== $type) {
            throw new CommonException('接口认证方式需为Bearer');
        }
        if (!$token || 'undefined' === $token) {
            throw new CommonException('尝试获取的Authorization信息不存在');
        }
    }

    private function generatePayload($data)
    {
        $basePayload = [
            'iss' => $this->iss, // 签发者
            'aud' => $this->aud, // 接收该JWT的一方
            'iat' => time(), // 签发时间
            'nbf' => time() + ($this->nbf ?? 0), // 某个时间点后才能访问
            'exp' => time() + $this->accessExp, // 过期时间
            'data' => $data // 自定义扩展信息
        ];
        $resPayLoad['accessPayload'] = $basePayload;
        $basePayload['exp'] = time() + $this->refreshExp;
        $resPayLoad['refreshPayload'] = $basePayload;
        return $resPayLoad;
    }

    /**
     * @desc: 生成令牌.
     * @param array $payload 载荷信息
     * @param string $secretKey 签名key
     * @param string $algorithms 算法
     * @return string
     */
    private function makeToken(array $payload): string
    {
        return BaseJWT::encode($payload, $this->secret, $this->algorithms);
    }

    private function verifyToken($token)
    {
        BaseJWT::$leeway = $this->leeway;
        $decoded = BaseJWT::decode($token, new Key($this->secret, $this->algorithms));
        $token = json_decode(json_encode($decoded), true);
        return $token;
    }
}